using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Research.Kinect.Nui;

namespace VirtualPostcard
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class VirtualPostcard : Microsoft.Xna.Framework.Game
    {
        enum PostcardMode
        {
            Postcard = 0,
            IRL,
            ElementIRL
        }
        #region XNA components
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        SpriteFont spriteFont;
        Dictionary<string, Texture2D> textures = new Dictionary<string, Texture2D>();
        byte[] backBuffer, frontBuffer;
        int fpsDrawnCounter = 0;
        int fpsSkippedCounter = 0;
        TimeSpan fpsTimeCounter = TimeSpan.Zero;
        #endregion

        #region Kinect components
        Runtime kinectRuntime;
        #endregion

        #region Application components
        PostcardMode postcardMode = PostcardMode.Postcard;
        byte depthLayerThreshold = 170;
        Vector4 depthLayerAdjustment;
        bool statusEnabled = false;
        string status, fpsStatus = "";
        #endregion

        public VirtualPostcard()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";
        }

        /// <summary>
        /// Default settings
        /// </summary>
        private void RestoreSettings(bool reset)
        {

            if (reset)
            {
                Program.OptInOrNot();
                depthLayerThreshold = 170;
                depthLayerAdjustment = new Vector4(-6.8f, -15.8f, 54f, 53f);
                SaveSettings();
            }
            else
            {
                depthLayerThreshold = Settings.Default.depthLayerThreshold;
                depthLayerAdjustment = Settings.Default.depthLayerAdjustment;
                switch (Settings.Default.Background)
                {
                    case "Kitchen":
                        GoToKitchen();
                        break;
                    case "Beach":
                    default:
                        GoToBeach();
                        break;
                }
            }
            try
            {
                postcardMode = (PostcardMode)Settings.Default.PostCardMode;
            }
            catch
            {
                postcardMode = PostcardMode.Postcard;
            }
        }

        private void SaveSettings()
        {
            Settings.Default.depthLayerThreshold = depthLayerThreshold;
            Settings.Default.depthLayerAdjustment = depthLayerAdjustment;
            Settings.Default.PostCardMode = (int)postcardMode;
            Settings.Default.Save();
        }


        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            this.Window.AllowUserResizing = true;
            graphics.PreferredBackBufferWidth = 640;
            graphics.PreferredBackBufferHeight = 400 + 52;
            graphics.ApplyChanges();
            try
            {
                kinectRuntime = new Runtime();

                kinectRuntime.Initialize(RuntimeOptions.UseDepthAndPlayerIndex | RuntimeOptions.UseSkeletalTracking | RuntimeOptions.UseColor);

                kinectRuntime.DepthStream.Open(ImageStreamType.Depth, 2, ImageResolution.Resolution320x240, ImageType.DepthAndPlayerIndex);
                kinectRuntime.VideoStream.Open(ImageStreamType.Video, 2, ImageResolution.Resolution640x480, ImageType.Color);

                backBuffer = new byte[kinectRuntime.VideoStream.Width * kinectRuntime.VideoStream.Height * 4];
                frontBuffer = new byte[kinectRuntime.VideoStream.Width * kinectRuntime.VideoStream.Height * 4];
                RestoreSettings(false);


                base.Initialize();
            }
            catch (Exception ex)
            {
                System.Windows.Forms.MessageBox.Show("Unable to initialize Kinect sensor\r\n" + ex.Message + "\r\n" + ex.StackTrace, "Initialize", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Error);
                this.Exit();
            }

        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            // Create a new SpriteBatch, which can be used to draw textures.
            spriteBatch = new SpriteBatch(GraphicsDevice);

            textures["backLayer"] = new Texture2D(GraphicsDevice, kinectRuntime.VideoStream.Width, kinectRuntime.VideoStream.Height);
            textures["frontLayer"] = new Texture2D(GraphicsDevice, kinectRuntime.VideoStream.Width, kinectRuntime.VideoStream.Height);
            spriteFont = Content.Load<SpriteFont>("Segoe");
        }

        /// <summary>
        /// UnloadContent will be called once per game and is the place to unload
        /// all content.
        /// </summary>
        protected override void UnloadContent()
        {
            foreach (Texture texture in textures.Values)
            {
                texture.Dispose();
            }
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            fpsTimeCounter += gameTime.ElapsedGameTime;
            IEnumerable<Keys> keys = new List<Keys>(Keyboard.GetState().GetPressedKeys());
            keys = keys.Except(new Keys[] { Keys.None });
            if (keys.Count() > 0)
            {

                #region Gets key pressed and modifier (shift, alt, ctrl...)
                Keys pressedKey = Keys.None;
                Keys pressedModifier = Keys.None;
                if (keys.Count() == 1)
                    pressedKey = keys.ElementAt(0);
                if (keys.Count() > 1)
                {
                    pressedKey = keys.ElementAt(0);
                    pressedModifier = keys.ElementAt(1);
                }

                #endregion

                switch (pressedKey)
                {

                    #region NUICamera Elevation Angle
                    case Keys.Up:
                        try
                        {
                            kinectRuntime.NuiCamera.ElevationAngle += 2;
                        }
                        catch { }
                        break;
                    case Keys.Down:
                        try
                        {
                            kinectRuntime.NuiCamera.ElevationAngle -= 2;
                        }
                        catch { }
                        break;
                    #endregion

                    #region layer Depth Threshold
                    case Keys.PageUp:
                        depthLayerThreshold += 1;
                        break;
                    case Keys.PageDown:
                        depthLayerThreshold -= 1;
                        break;
                    #endregion

                    #region Transform (X,Y,W,H) depth stream to match video stream
                    case Keys.Z:
                        if (pressedModifier == Keys.None)
                            depthLayerAdjustment.Y += 0.2f;
                        else
                            depthLayerAdjustment.W += 0.2f * (1f + (depthLayerAdjustment.Z == -1 ? 1f : 0));
                        break;

                    case Keys.S:
                        if (pressedModifier == Keys.None)
                            depthLayerAdjustment.Y -= 0.2f;
                        else
                            depthLayerAdjustment.W -= 0.2f * (1f + (depthLayerAdjustment.Z == 1 ? -1f : 0));
                        break;

                    case Keys.Q:
                        if (pressedModifier == Keys.None)
                            depthLayerAdjustment.X += 0.2f;
                        else
                            depthLayerAdjustment.Z += 0.2f * (1f + (depthLayerAdjustment.W == -1 ? 1f : 0));
                        break;

                    case Keys.D:
                        if (pressedModifier == Keys.None)
                            depthLayerAdjustment.X -= 0.2f;
                        else
                            depthLayerAdjustment.Z -= 0.2f * (1f + (depthLayerAdjustment.W == 1 ? -1f : 0));
                        break;

                    #endregion

                    case Keys.T:
                        statusEnabled = !statusEnabled;
                        break;

                    case Keys.B:
                        if (pressedModifier == Keys.None)
                        {
                            GoToBeach();
                        }
                        else
                        {
                            GoToKitchen();
                        }
                        break;

                    case Keys.R:
                        if (pressedModifier == Keys.None)
                            RestoreSettings(false);
                        else
                            RestoreSettings(true);
                        break;

                    // Shows postcard background
                    case Keys.M:
                        if (pressedModifier == Keys.None)
                            EnablePostcardMode();
                        else if (pressedModifier == Keys.LeftControl || pressedModifier == Keys.RightControl)
                            EnableIRLMode();
                        else if (pressedModifier == Keys.LeftShift || pressedModifier == Keys.RightShift)
                            EnableElementIRL();
                        break;

                    // Allows the game to exit
                    case Keys.Escape:
                    case Keys.Back:
                        this.Exit();
                        SaveSettings();
                        break;
                }
            }

            status = string.Format("DepthLayerAdjustment:{0}\r\nDepthLayerThreshold:{1}\r\n{2}",
                depthLayerAdjustment.ToString(),
                depthLayerThreshold,
                fpsStatus);
            base.Update(gameTime);
        }

        private void GoToKitchen()
        {
            textures["backgroundLayer"] = Content.Load<Texture2D>("kitchen-decor");
            textures["mainLayer"] = Content.Load<Texture2D>("kitchen-table");
            Settings.Default.Background = "Kitchen";
        }

        private void GoToBeach()
        {
            textures["backgroundLayer"] = Content.Load<Texture2D>("backgroundLayer");
            textures["mainLayer"] = Content.Load<Texture2D>("mainLayer");
            Settings.Default.Background = "Beach";
        }

        private void EnablePostcardMode()
        {
            postcardMode = PostcardMode.Postcard;
        }

        private void EnableIRLMode()
        {
            postcardMode = PostcardMode.IRL;
        }

        private void EnableElementIRL()
        {
            postcardMode = PostcardMode.ElementIRL;
        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);
            // release last texture from GraphicsDevice memory by drawing an empty one
            GraphicsDevice.Textures[0] = null;
            fpsDrawnCounter++;
            try
            {
                ImageFrame depthFrame = kinectRuntime.DepthStream.GetNextFrame(20);
                ImageFrame videoFrame = kinectRuntime.VideoStream.GetNextFrame(20);
                if (depthFrame != null && videoFrame != null)
                {
                    PrepareBuffers(videoFrame, depthFrame);
                    textures["backLayer"].SetData(backBuffer);
                    textures["frontLayer"].SetData(frontBuffer);
                }
                else
                {
                    fpsSkippedCounter++;
                }
            }
            catch (Exception ex)
            {
                System.Windows.Forms.MessageBox.Show(ex.Message, "Initialize", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Error);
            }

            if (fpsTimeCounter.TotalSeconds > 1)
            {
                fpsStatus = fpsDrawnCounter + "/" + fpsSkippedCounter;
                fpsTimeCounter = TimeSpan.Zero;
                fpsDrawnCounter = fpsSkippedCounter = 0;
            }

            spriteBatch.Begin();

            spriteBatch.Draw(textures["backgroundLayer"], GraphicsDevice.Viewport.Bounds, Color.White);
            spriteBatch.Draw(textures["backLayer"], GraphicsDevice.Viewport.Bounds, Color.White);
            spriteBatch.Draw(textures["mainLayer"], GraphicsDevice.Viewport.Bounds, Color.White);
            spriteBatch.Draw(textures["frontLayer"], GraphicsDevice.Viewport.Bounds, Color.White);
            if (statusEnabled)
                spriteBatch.DrawString(spriteFont, status, new Vector2(10, 10), Color.DarkGreen);
            spriteBatch.End();

            base.Draw(gameTime);
        }

        /// <summary>
        /// Extracts players (edges and color) and injects them in Front and Back buffers
        /// </summary>
        /// <param name="videoFrame"></param>
        /// <param name="depthFrame"></param>
        private void PrepareBuffers(ImageFrame videoFrame, ImageFrame depthFrame)
        {
            for (int y = 0; y < videoFrame.Image.Height; y++)
            {
                int videoYOffset = y * kinectRuntime.VideoStream.Width;
                int depthYOffset = GetAdjustedDepthYOffset(y);
                for (int x = 0; x < videoFrame.Image.Width; x++)
                {
                    int index = (x + videoYOffset) * videoFrame.Image.BytesPerPixel;
                    byte b = videoFrame.Image.Bits[index];
                    byte g = videoFrame.Image.Bits[index + 1];
                    byte r = videoFrame.Image.Bits[index + 2];


                    backBuffer[index] = backBuffer[index + 1] = backBuffer[index + 2] = backBuffer[index + 3] = (byte)(postcardMode == PostcardMode.Postcard ? 0 : 255);
                    frontBuffer[index] = frontBuffer[index + 1] = frontBuffer[index + 2] = frontBuffer[index + 3] = 0;

                    int depthXOffset = GetAdjustedDepthXOffset(x, false);
                    if (depthXOffset < 0 || depthXOffset > depthFrame.Image.Width
                        || depthYOffset < 0)
                        continue;
                    int indexDepth = depthFrame.Image.BytesPerPixel * (depthXOffset + depthYOffset);
                    if (indexDepth >= depthFrame.Image.Bits.Length)
                        continue;
                    byte player = (byte)((depthFrame.Image.Bits[indexDepth] & 0x07) % 3);

                    if (postcardMode == PostcardMode.IRL)
                    {
                        frontBuffer[index] = r;
                        frontBuffer[index + 1] = g;
                        frontBuffer[index + 2] = b;
                        frontBuffer[index + 3] = 255;
                    }
                    else
                    {
                        if (postcardMode == PostcardMode.ElementIRL)
                        {
                            backBuffer[index] = r;
                            backBuffer[index + 1] = g;
                            backBuffer[index + 2] = b;
                            backBuffer[index + 3] = 255;
                        }

                        if (player > 0) // player
                        {
                            byte depth = CalculateIntensityFromDepth(depthFrame, indexDepth);

                            if (depth < depthLayerThreshold)
                            {
                                if (postcardMode == PostcardMode.Postcard)
                                {
                                    backBuffer[index] = r;
                                    backBuffer[index + 1] = g;
                                    backBuffer[index + 2] = b;
                                    backBuffer[index + 3] = 255;
                                }
                            }
                            else
                            {
                                frontBuffer[index] = r;
                                frontBuffer[index + 1] = g;
                                frontBuffer[index + 2] = b;
                                frontBuffer[index + 3] = 255;
                            }
                        }
                        if (statusEnabled && postcardMode == PostcardMode.Postcard && x % 5 == 0 && y % 5 == 0)
                        {

                            int thumbIndex = ((GraphicsDevice.Viewport.Bounds.Width - 128) * 5 + x + videoYOffset) * videoFrame.Image.BytesPerPixel / 5;
                            frontBuffer[thumbIndex] = r;
                            frontBuffer[thumbIndex + 1] = g;
                            frontBuffer[thumbIndex + 2] = b;
                            frontBuffer[thumbIndex + 3] = 255;
                        }
                    }
                }
            }
        }

        private int GetAdjustedDepthXOffset(int x, bool mirrored)
        {
            return (int)((mirrored ? (kinectRuntime.VideoStream.Width - x) : x)
                / (kinectRuntime.VideoStream.Width / kinectRuntime.DepthStream.Width) * depthLayerAdjustment.Z / 50f + depthLayerAdjustment.X);
        }

        private int GetAdjustedDepthYOffset(int y)
        {
            return (int)(y / (kinectRuntime.VideoStream.Height / kinectRuntime.DepthStream.Height) * depthLayerAdjustment.W / 50f + depthLayerAdjustment.Y) * kinectRuntime.DepthStream.Width;
        }

        const float MaxDepthDistance = 4000; // max value returned
        const float MinDepthDistance = 800; // min value returned
        const float MaxDepthDistanceOffset = MaxDepthDistance - MinDepthDistance;

        private byte CalculateIntensityFromDepth(ImageFrame depthFrame, int indexDepth)
        {
            int distance = 0;
            if (depthFrame.Type == ImageType.DepthAndPlayerIndex)
                distance = (int)(depthFrame.Image.Bits[indexDepth] >> 3 | depthFrame.Image.Bits[indexDepth + 1] << 5);
            else // depth only (12 bits)
                distance = (int)(depthFrame.Image.Bits[indexDepth] | depthFrame.Image.Bits[indexDepth + 1] << 8);

            //formula for calculating monochrome intensity for histogram
            return (byte)(255 - (255 * Math.Max(distance - MinDepthDistance, 0) / (MaxDepthDistanceOffset)));
        }
    }
}
